/*
 * Copyright 2014 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.usergrid.corepersistence;


import com.google.common.base.Optional;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import org.apache.commons.lang.StringUtils;
import org.apache.usergrid.corepersistence.asyncevents.AsyncEventService;
import org.apache.usergrid.corepersistence.index.CollectionSettingsFactory;
import org.apache.usergrid.corepersistence.index.ReIndexRequestBuilder;
import org.apache.usergrid.corepersistence.index.ReIndexService;
import org.apache.usergrid.corepersistence.service.CollectionService;
import org.apache.usergrid.corepersistence.service.ConnectionService;
import org.apache.usergrid.corepersistence.util.CpNamingUtils;
import org.apache.usergrid.exception.ConflictException;
import org.apache.usergrid.locking.LockManager;
import org.apache.usergrid.mq.QueueManagerFactory;
import org.apache.usergrid.persistence.*;
import org.apache.usergrid.persistence.actorsystem.ActorSystemFig;
import org.apache.usergrid.persistence.actorsystem.ActorSystemManager;
import org.apache.usergrid.persistence.cassandra.CassandraService;
import org.apache.usergrid.persistence.cassandra.CounterUtils;
import org.apache.usergrid.persistence.cassandra.Setup;
import org.apache.usergrid.persistence.collection.EntityCollectionManager;
import org.apache.usergrid.persistence.collection.exception.CollectionRuntimeException;
import org.apache.usergrid.persistence.collection.serialization.impl.migration.EntityIdScope;
import org.apache.usergrid.persistence.collection.uniquevalues.UniqueValuesService;
import org.apache.usergrid.persistence.core.metrics.MetricsFactory;
import org.apache.usergrid.persistence.core.migration.data.MigrationDataProvider;
import org.apache.usergrid.persistence.core.scope.ApplicationScope;
import org.apache.usergrid.persistence.core.scope.ApplicationScopeImpl;
import org.apache.usergrid.persistence.core.util.Health;
import org.apache.usergrid.persistence.entities.Application;
import org.apache.usergrid.persistence.exceptions.ApplicationAlreadyExistsException;
import org.apache.usergrid.persistence.exceptions.DuplicateUniquePropertyExistsException;
import org.apache.usergrid.persistence.exceptions.EntityNotFoundException;
import org.apache.usergrid.persistence.graph.*;
import org.apache.usergrid.persistence.graph.impl.SimpleSearchByEdgeType;
import org.apache.usergrid.persistence.index.EntityIndex;
import org.apache.usergrid.persistence.model.entity.Id;
import org.apache.usergrid.persistence.model.entity.SimpleId;
import org.apache.usergrid.persistence.model.util.UUIDGenerator;
import org.apache.usergrid.utils.UUIDUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import rx.Observable;

import java.util.*;

import static java.lang.String.CASE_INSENSITIVE_ORDER;
import static org.apache.usergrid.persistence.Schema.PROPERTY_NAME;
import static org.apache.usergrid.persistence.Schema.TYPE_APPLICATION;


/**
 * Implement good-old Usergrid EntityManagerFactory with the new-fangled Core Persistence API.
 * This is where we keep track of applications and system properties.
 */
public class CpEntityManagerFactory implements EntityManagerFactory, ApplicationContextAware {

    private static final Logger logger = LoggerFactory.getLogger( CpEntityManagerFactory.class );

    private final EntityManagerFig entityManagerFig;
    private final ActorSystemFig actorSystemFig;

    private ApplicationContext applicationContext;

    private Setup setup = null;

    EntityManager managementAppEntityManager = null;

    // cache of already instantiated entity managers
    private final String ENTITY_MANAGER_CACHE_SIZE = "entity.manager.cache.size";
    private final LoadingCache<UUID, EntityManager> entityManagers;

    private final ApplicationIdCache applicationIdCache;

    Application managementApp = null;

    private ManagerCache managerCache;

    private CassandraService cassandraService;
    private CounterUtils counterUtils;
    private Injector injector;
    private final ReIndexService reIndexService;
    private final MetricsFactory metricsFactory;
    private final AsyncEventService indexService;
    private final CollectionService collectionService;
    private final ConnectionService connectionService;
    private final GraphManagerFactory graphManagerFactory;
    private final CollectionSettingsFactory collectionSettingsFactory;
    private ActorSystemManager actorSystemManager;
    private UniqueValuesService uniqueValuesService;
    private final LockManager lockManager;

    private final QueueManagerFactory queueManagerFactory;

    public static final String MANAGEMENT_APP_INIT_MAXRETRIES= "management.app.init.max-retries";
    public static final String MANAGEMENT_APP_INIT_INTERVAL = "management.app.init.interval";


    public CpEntityManagerFactory(
        final CassandraService cassandraService, final CounterUtils counterUtils, final Injector injector ) {

        this.cassandraService = cassandraService;
        this.counterUtils = counterUtils;
        this.injector = injector;
        this.reIndexService = injector.getInstance(ReIndexService.class);
        this.entityManagerFig = injector.getInstance(EntityManagerFig.class);
        this.actorSystemFig = injector.getInstance( ActorSystemFig.class );
        this.managerCache = injector.getInstance( ManagerCache.class );
        this.metricsFactory = injector.getInstance( MetricsFactory.class );
        this.indexService = injector.getInstance( AsyncEventService.class );
        this.graphManagerFactory = injector.getInstance( GraphManagerFactory.class );
        this.collectionService = injector.getInstance( CollectionService.class );
        this.connectionService = injector.getInstance( ConnectionService.class );
        this.collectionSettingsFactory = injector.getInstance( CollectionSettingsFactory.class );

        Properties properties = cassandraService.getProperties();
        this.entityManagers = createEntityManagerCache( properties );

        logger.info("EntityManagerFactoring starting...");

        if ( actorSystemFig.getEnabled() ) {
            try {
                logger.info("Akka cluster starting...");

                this.uniqueValuesService = injector.getInstance( UniqueValuesService.class );
                this.actorSystemManager = injector.getInstance( ActorSystemManager.class );

                actorSystemManager.registerRouterProducer( uniqueValuesService );
                actorSystemManager.start();
                actorSystemManager.waitForClientActor();

            } catch (Throwable t) {
                logger.error("Error starting Akka", t);
                throw t;
            }
        }
        this.lockManager = injector.getInstance( LockManager.class );
        this.queueManagerFactory = injector.getInstance( QueueManagerFactory.class );


        // this line always needs to be last due to the temporary cicular dependency until spring is removed
        this.applicationIdCache = injector.getInstance(ApplicationIdCacheFactory.class).getInstance(
            getManagementEntityManager() );

        checkManagementApp( properties );
    }


    private LoadingCache<UUID, EntityManager> createEntityManagerCache(Properties properties) {

        int entityManagerCacheSize = 100;
        try {
            entityManagerCacheSize = Integer.parseInt( properties.getProperty( ENTITY_MANAGER_CACHE_SIZE, "100" ));
        } catch ( Exception e ) {
            logger.error("Error parsing " + ENTITY_MANAGER_CACHE_SIZE + ". Will use " + entityManagerCacheSize, e );
        }

        return CacheBuilder.newBuilder()
            .maximumSize(entityManagerCacheSize)
            .build(new CacheLoader<UUID, EntityManager>() {

                public EntityManager load( UUID appId ) { // no checked exception

                    // create new entity manager and pre-fetch its application
                    EntityManager entityManager = _getEntityManager( appId );
                    Application app = null;
                    Throwable throwable = null;
                    try {
                        app = entityManager.getApplication();
                    } catch (Throwable t) {
                        throwable = t;
                    }

                    // the management app is a special case
                    if ( CpNamingUtils.MANAGEMENT_APPLICATION_ID.equals( appId ) ) {

                        if ( app != null ) {
                            // we successfully fetched up the management app, cache it for a rainy day
                            managementAppEntityManager = entityManager;

                        } else if ( managementAppEntityManager != null ) {
                            // failed to fetch management app, use cached one
                            entityManager = managementAppEntityManager;
                            logger.error("Failed to fetch management app");
                        }
                    }

                    // missing keyspace means we have not done bootstrap yet
                    final boolean isBootstrapping;
                    if ( throwable instanceof CollectionRuntimeException ) {
                        CollectionRuntimeException cre = (CollectionRuntimeException) throwable;
                        isBootstrapping = cre.isBootstrapping();
                    } else {
                        isBootstrapping = false;
                    }

                    // work around for https://issues.apache.org/jira/browse/USERGRID-1291
                    // throw exception so that we do not cache
                    // TODO: determine how application name can intermittently be null
                    if ( app != null && app.getName() == null ) {
                        throw new RuntimeException( "Name is null for application " + appId, throwable );
                    }

                    if ( app == null && !isBootstrapping ) {
                        throw new RuntimeException( "Error getting application " + appId, throwable );

                    } // else keyspace is missing because setup/bootstrap not done yet

                    return entityManager;
                }
            });
    }


    private void checkManagementApp(Properties properties) {

        int maxRetries = 100;
        try {
            maxRetries = Integer.parseInt( properties.getProperty( MANAGEMENT_APP_INIT_MAXRETRIES, "100" ));

        } catch ( Exception e ) {
            logger.error("Error parsing " + MANAGEMENT_APP_INIT_MAXRETRIES + ". Will use " + maxRetries, e );
        }

        int interval = 1000;
        try {
            interval = Integer.parseInt( properties.getProperty( MANAGEMENT_APP_INIT_INTERVAL, "1000" ));

        } catch ( Exception e ) {
            logger.error("Error parsing " + MANAGEMENT_APP_INIT_INTERVAL + ". Will use " + maxRetries, e );
        }

        // hold up construction until we can access the management app
        int retries = 0;
        boolean managementAppFound = false;
        boolean bootstrapping = false;
        Set<Class> seenBefore = new HashSet<>(10);
        while ( !managementAppFound && retries++ < maxRetries ) {
            try {
                // bypass entity manager cache and get managementApp
                managementApp = _getEntityManager( getManagementAppId() ).getApplication();
                managementAppFound = true;

            } catch ( Throwable t ) {

                if ( t instanceof CollectionRuntimeException ) {
                    CollectionRuntimeException cre = (CollectionRuntimeException)t;
                    if ( cre.isBootstrapping() ) {
                        // we're bootstrapping, ignore this and continue
                        bootstrapping = true;
                        break;
                    }
                }
                Throwable cause = t;

                // there was an error, be as informative as possible
                StringBuilder sb = new StringBuilder();
                sb.append(retries).append(": Error (");

                if ( t instanceof UncheckedExecutionException ) {
                    UncheckedExecutionException uee = (UncheckedExecutionException)t;
                    if ( uee.getCause() instanceof RuntimeException ) {
                        cause = uee.getCause().getCause();
                        sb.append(cause.getClass().getSimpleName()).append(") ")
                          .append(uee.getCause().getMessage());
                    } else {
                        cause = uee.getCause();
                        sb.append(cause.getClass().getSimpleName()).append(") ").append(t.getMessage());
                    }
                } else {
                    sb.append(t.getCause().getClass().getSimpleName()).append(") ").append(t.getMessage());
                }

                String msg = sb.toString();
                if ( !seenBefore.contains( cause.getClass() ) ) {
                    logger.error( msg, t);
                } else {
                    logger.error(msg);
                }
                seenBefore.add( cause.getClass() );

                try { Thread.sleep( interval ); } catch (InterruptedException ignored) {}
            }
        }

        if ( !managementAppFound && !bootstrapping ) {
            // exception here will prevent WAR from being deployed
            throw new RuntimeException( "Unable to get management app after " + retries + " retries" );
        }
    }


    public CounterUtils getCounterUtils() {
        return counterUtils;
    }


    public CassandraService getCassandraService() {
        return cassandraService;
    }


    private void initMgmtAppInternal() {

        EntityManager em = getEntityManager(getManagementAppId());
        indexService.queueInitializeApplicationIndex(CpNamingUtils.getApplicationScope(getManagementAppId()));

        try {
            if ( em.getApplication() == null ) {
                logger.info("Creating management application");
                Map mgmtAppProps = new HashMap<String, Object>();
                mgmtAppProps.put(PROPERTY_NAME, CassandraService.MANAGEMENT_APPLICATION);
                em.create( getManagementAppId(), TYPE_APPLICATION, mgmtAppProps);
                em.getApplication();
            }

        } catch (Exception ex) {
            throw new RuntimeException("Fatal error creating management application", ex);
        }
    }


    private Observable<EntityIdScope> getAllEntitiesObservable(){
      return injector.getInstance( Key.get(new TypeLiteral< MigrationDataProvider<EntityIdScope>>(){})).getData();
    }


    @Override
    public EntityManager getEntityManager(UUID applicationId) {
        try {
            return entityManagers.get( applicationId );
        }
        catch ( Throwable t ) {
            logger.error("Error getting entity manager", t);
        }
        return _getEntityManager(applicationId);
    }


    private EntityManager _getEntityManager( UUID applicationId ) {

        EntityManager em = new CpEntityManager(
            cassandraService,
            counterUtils,
            indexService,
            managerCache,
            metricsFactory,
            actorSystemFig,
            entityManagerFig,
            graphManagerFactory,
            collectionService,
            connectionService,
            collectionSettingsFactory,
            applicationId,
            queueManagerFactory);

        return em;
    }

    @Override
    public Entity createApplicationV2(String organizationName, String name) throws Exception {
        return createApplicationV2( organizationName, name, null, null, false);
    }


    @Override
    public Entity createApplicationV2(
        String orgName, String name, UUID applicationId, Map<String, Object> properties, boolean forMigration) throws Exception {

        String appName = buildAppName( orgName, name );

        final UUID appId = applicationIdCache.getApplicationId( appName );

        if ( appId != null ) {
            throw new ApplicationAlreadyExistsException( name );
        }

        applicationId = applicationId==null ?  UUIDGenerator.newTimeUUID() : applicationId;

        if (logger.isDebugEnabled()) {
            logger.debug("New application orgName {} orgAppName {} id {} ",
                orgName, name, applicationId.toString());
        }

        return initializeApplicationV2( orgName, applicationId, appName, properties, forMigration);
    }



    private String buildAppName( String organizationName, String name ) {
        return StringUtils.lowerCase(name.contains("/") ? name : organizationName + "/" + name);
    }


    /**
     * @return UUID of newly created Entity of type application_info
     */
    @Override
    public Entity initializeApplicationV2(String organizationName, final UUID applicationId, String name,
                                          Map<String, Object> properties, boolean forMigration) throws Exception {

        // Ensure the management application is initialized
        initMgmtAppInternal();

        // Get entity managers by bypassing the entity manager cache because it expects apps to already exist
        final EntityManager managementEm = _getEntityManager( getManagementAppId() );
        EntityManager appEm = _getEntityManager(applicationId);

        final String appName = buildAppName(organizationName, name);

        // check for pre-existing application

        if ( lookupApplication( appName ) != null ) {
            throw new ApplicationAlreadyExistsException( appName );
        }

        // Initialize the index for this new application
        appEm.initializeIndex();
        indexService.queueInitializeApplicationIndex(CpNamingUtils.getApplicationScope(applicationId));
        if ( properties == null ) {
            properties = new TreeMap<>( CASE_INSENSITIVE_ORDER);
        }
        properties.put( PROPERTY_NAME, appName );
        appEm.create(applicationId, TYPE_APPLICATION, properties);

        // only reset roles if this application isn't being migrated (meaning dictionary and role data already exists)
        if(!forMigration){
            appEm.resetRoles();
        }



        // create application info entity in the management app

        Map<String, Object> appInfoMap = new HashMap<String, Object>() {{
            put( PROPERTY_NAME, appName );
            put( "org", organizationName );
        }};

        Entity appInfo;
        try {
            appInfo = managementEm.create(new SimpleId(applicationId,CpNamingUtils.APPLICATION_INFO), appInfoMap);
        } catch (DuplicateUniquePropertyExistsException e) {
            throw new ApplicationAlreadyExistsException(appName);
        }

        // evict app Id from cache
        applicationIdCache.evictAppId(appName);

        logger.info("Initialized application {}", appName);
        return appInfo;
    }



    /**
     * Delete Application.
     *
     * <p>The Application Entity is be moved to a Deleted_Applications collection and the
     * Application index will be removed.
     *
     * <p>TODO: add scheduled task that can completely delete all deleted application data.</p>
     *
     * @param applicationId UUID of Application to be deleted.
     */
    @Override
    public void deleteApplication(UUID applicationId) throws Exception {

        // find application_info for application to delete

        migrateAppInfo(applicationId, CpNamingUtils.APPLICATION_INFO, CpNamingUtils.DELETED_APPLICATION_INFOS, CpNamingUtils.DELETED_APPLICATION_INFO).toBlocking()
            .lastOrDefault( null );
    }

    //TODO: return status for restore
    @Override
    public Entity restoreApplication(UUID applicationId) throws Exception {

        // get the deleted_application_info for the deleted app
        return (Entity) migrateAppInfo( applicationId, CpNamingUtils.DELETED_APPLICATION_INFO,
            CpNamingUtils.APPLICATION_INFOS , CpNamingUtils.APPLICATION_INFO ).lastOrDefault( null )
             .map( appInfo -> {

                 //start the index rebuild
                 final ReIndexRequestBuilder builder = reIndexService.getBuilder().withApplicationId( applicationId );
                 reIndexService.rebuildIndex( builder );

                 //load the entity
                 final EntityManager managementEm = getEntityManager( getManagementAppId() );
                 try {
                     return managementEm.get( new SimpleEntityRef( CpNamingUtils.APPLICATION_INFO, applicationId ) );
                 }
                 catch ( Exception e ) {
                     logger.error( "Failed to get entity", e );
                     throw new RuntimeException( e );
                 }
             } )
            .toBlocking().lastOrDefault(null);

    }

//    @Override


    /**
     * Migrate the application from one type to another.  Used in delete and restore
     * @param applicationUUID The applicationUUID
     * @param deleteTypeName The type to use on the delete
     * @param createCollectionName The name of the collection to write the entity into
     * @param createTypeName The type to use on the create
     * @return
     * @throws Exception
     */
    private Observable migrateAppInfo(final UUID applicationUUID,  final String deleteTypeName, final String createCollectionName, final String createTypeName ) throws Exception {

        final ApplicationScope managementAppScope = CpNamingUtils.getApplicationScope(CpNamingUtils.MANAGEMENT_APPLICATION_ID);
        final EntityManager managementEm = getEntityManager(CpNamingUtils.MANAGEMENT_APPLICATION_ID);

        //the application id we will be removing
        final Id deleteApplicationId = new SimpleId(applicationUUID, deleteTypeName );

        //the application id we'll be creating
        final Id createApplicationId = new SimpleId( applicationUUID, createTypeName );

        //the application scope of the deleted app to clean it's index
        final ApplicationScope deleteApplicationScope = new ApplicationScopeImpl(deleteApplicationId);

        Entity oldAppEntity = managementEm.get(new SimpleEntityRef( deleteTypeName, applicationUUID));

        if(oldAppEntity == null){
            throw new EntityNotFoundException( String.format("Could not find application with UUID '%s'", applicationUUID) );
        }


        // ensure that there is not already a deleted app with the same name

        final EntityRef alias = managementEm.getAlias( createCollectionName, oldAppEntity.getName() );
        if ( alias != null ) {
            throw new ConflictException( "Cannot delete app with same name as already deleted app" );
        }
        // make a copy of the app to delete application_info entity
        // and put it in a deleted_application_info collection

        final Entity newAppEntity =
            managementEm.create( new SimpleId( applicationUUID, createTypeName ), oldAppEntity.getProperties() );

        // copy its connections too

        final Set<String> connectionTypes = managementEm.getConnectionTypes( oldAppEntity );
        Observable copyConnections = Observable.from( connectionTypes ).doOnNext( connType -> {
            try {
                final Results connResults =
                    managementEm.getTargetEntities( oldAppEntity, connType, null, Query.Level.ALL_PROPERTIES );
                connResults.getEntities().forEach( entity -> {
                    try {
                        managementEm.createConnection( newAppEntity, connType, entity );
                    }
                    catch ( Exception e ) {
                        throw new RuntimeException( e );
                    }
                } );
            }
            catch ( Exception e ) {
                throw new RuntimeException( e );
            }
        } );

        final Id managementAppId = CpNamingUtils.getManagementApplicationId();
        final EntityIndex aei = getManagementIndex();
        final GraphManager managementGraphManager = managerCache.getGraphManager(managementAppScope);
        final Edge createEdge = CpNamingUtils.createCollectionEdge(managementAppId, createCollectionName, createApplicationId);

        final Observable createNodeGraph = managementGraphManager.writeEdge(createEdge);

        final Observable deleteAppFromIndex = aei.deleteApplication();

        return Observable
            .merge( copyConnections, createNodeGraph, deleteAppFromIndex )
            .doOnCompleted( () -> {
                try {
                    if ( oldAppEntity != null ) {
                        managementEm.delete( oldAppEntity );
                        applicationIdCache.evictAppId( oldAppEntity.getName() );
                    }
                    EntityIndex ei = getManagementIndex();
                    ei.refreshAsync().toBlocking().last();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }

            } );
    }


    @Override
    public UUID importApplication(
            String organization, UUID applicationId,
            String name, Map<String, Object> properties) throws Exception {

        throw new UnsupportedOperationException("Not supported yet.");
    }


    public UUID lookupApplication(String orgAppName ) throws Exception {
        return applicationIdCache.getApplicationId(orgAppName);
    }


    @Override
    public Map<String, UUID> getApplications() throws Exception {
        return getApplications( CpNamingUtils.getEdgeTypeFromCollectionName( CpNamingUtils.APPLICATION_INFOS ) );
    }


    @Override
    public Map<String, UUID> getDeletedApplications() throws Exception {
        return getApplications( CpNamingUtils.getEdgeTypeFromCollectionName( CpNamingUtils.DELETED_APPLICATION_INFOS ) );
    }


    private Map<String, UUID> getApplications(final String edgeType) throws Exception {

        ApplicationScope appScope =
            CpNamingUtils.getApplicationScope(CpNamingUtils.MANAGEMENT_APPLICATION_ID);
        GraphManager gm = managerCache.getGraphManager(appScope);

        EntityManager managementEM = getEntityManager(CpNamingUtils.MANAGEMENT_APPLICATION_ID);
        Application managementApp = managementEM.getApplication();
        if( managementApp == null ) {
            throw new RuntimeException("Management App "
                + CpNamingUtils.MANAGEMENT_APPLICATION_ID + " should never be null");
        }
        Id managementId = new SimpleId( managementApp.getUuid(), managementApp.getType() );


        if (logger.isDebugEnabled()) {
            logger.debug("getApplications(): Loading edges of edgeType {} from {}:{}",
                edgeType, managementId.getType(), managementId.getUuid());
        }

        Observable<MarkedEdge> edges = gm.loadEdgesFromSource(
            new SimpleSearchByEdgeType( managementId, edgeType, Long.MAX_VALUE, SearchByEdgeType.Order.DESCENDING,
                Optional.<Edge>absent() ) );

        final EntityCollectionManager ecm = managerCache.getEntityCollectionManager( appScope );

        //buffer our edges and batch fetch the app infos for faster I/O
        return edges.map( edge -> {
            return edge.getTargetNode();
        } ).buffer( 100 ).flatMap( entityIds -> {
            return ecm.load( entityIds );
        } )
                    .flatMap( entitySet -> Observable.from( entitySet.getEntities() ) )
            //collect all the app infos into a single map for return
                    .collect( () -> new HashMap<String, UUID>(), ( appMap, entity ) -> {

                            if ( !entity.getEntity().isPresent() ) {
                                return;
                            }

                            final org.apache.usergrid.persistence.model.entity.Entity entityData =
                                entity.getEntity().get();

                            final UUID applicationId = entity.getId().getUuid();
                            final String applicationName = ( String ) entityData.getField( PROPERTY_NAME ).getValue();

                            appMap.put( applicationName , applicationId );
                        } ).toBlocking().last();
    }


    @Override
    public void setup() throws Exception {
        getSetup().initSchema();
        lockManager.setup();
    }


    @Override
    public void bootstrap() throws Exception {

        // Always make sure the database schema is initialized
        getSetup().initSchema();

        // Roll the new 2.x Migration classes to the latest version supported
        getSetup().runDataMigration();

        // Make sure the management application is created
        initMgmtAppInternal();

        // Ensure management app is initialized
        getSetup().initMgmtApp();

    }


    @Override
    public Map<String, String> getServiceProperties() {

        Map<String, String> props = new HashMap<String,String>();

        EntityManager em = getEntityManager(getManagementAppId());
        Query q = Query.fromQL("select *");
        Results results = null;
        try {
            results = em.searchCollection( em.getApplicationRef(), "propertymaps", q);

        } catch (Exception ex) {
            logger.error("Error getting system properties", ex);
        }

        if ( results == null || results.isEmpty() ) {
            return props;
        }

        org.apache.usergrid.persistence.Entity e = results.getEntity();
        for ( String key : e.getProperties().keySet() ) {
            props.put( key, props.get(key).toString() );
        }
        return props;
    }


    @Override
    public boolean updateServiceProperties(Map<String, String> properties) {

        EntityManager em = getEntityManager(getManagementAppId());
        Query q = Query.fromQL("select *");
        Results results = null;
        try {
            results = em.searchCollection( em.getApplicationRef(), "propertymaps", q);

        } catch (Exception ex) {
            logger.error("Error getting system properties", ex);
            return false;
        }

        org.apache.usergrid.persistence.Entity propsEntity = null;

        if ( !results.isEmpty() ) {
            propsEntity = results.getEntity();

        } else {
            propsEntity = EntityFactory.newEntity( UUIDUtils.newTimeUUID(), "propertymap");
        }

        // intentionally going only one-level deep into fields and treating all
        // values as strings because that is all we need for service properties
        for ( String key : properties.keySet() ) {
            propsEntity.setProperty(key, properties.get(key).toString());
        }

        try {
            em.update( propsEntity );

        } catch (Exception ex) {
            logger.error("Error updating service properties", ex);
            return false;
        }

        return true;
    }


    @Override
    public boolean setServiceProperty(final String name, final String value) {
        return updateServiceProperties(new HashMap<String, String>() {{
            put(name, value);
        }});
    }


    @Override
    public boolean deleteServiceProperty(String name) {

        EntityManager em = getEntityManager(getManagementAppId());


        Query q = Query.fromQL( "select *");
        Results results = null;
        try {
            results = em.searchCollection( em.getApplicationRef(), "propertymaps", q);

        } catch (Exception ex) {
            logger.error("Error getting service property for delete of property: {}", name, ex);
            return false;
        }

        org.apache.usergrid.persistence.Entity propsEntity = null;

        if ( !results.isEmpty() ) {
            propsEntity = results.getEntity();

        } else {
            propsEntity = EntityFactory.newEntity( UUIDUtils.newTimeUUID(), "propertymap");
        }

        try {
            ((AbstractEntity)propsEntity).clearDataset( name );
            em.update( propsEntity );

        } catch (Exception ex) {
            logger.error("Error deleting service property orgAppName: {}", name, ex);
            return false;
        }

        return true;
    }

    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @Override
    public void setApplicationContext( ApplicationContext applicationContext ) throws BeansException {
        this.applicationContext = applicationContext;
//        try {
//            setup();
//        } catch (Exception ex) {
//            logger.error("Error setting up EMF", ex);
//        }
    }


    @Override
    public long performEntityCount() {
        //TODO, this really needs to be a task that writes this data somewhere since this will get
        //progressively slower as the system expands
        return (Long) getAllEntitiesObservable().countLong().toBlocking().last();
    }



    @Override
    public UUID getManagementAppId() {
        return CpNamingUtils.MANAGEMENT_APPLICATION_ID;
    }

    @Override
    public EntityManager getManagementEntityManager() {
        return getEntityManager(CpNamingUtils.MANAGEMENT_APPLICATION_ID);
    }


    /**
     * Gets the setup.
     * @return Setup helper
     */
    public Setup getSetup() {
        if ( setup == null ) {
            setup = new CpSetup( this, cassandraService, injector );
        }
        return setup;
    }


    /**
     * TODO, these 3 methods are super janky.  During refactoring we should clean this model up
     */
    public EntityIndex.IndexRefreshCommandInfo refreshIndex(UUID applicationId) {
        return getEntityManager(applicationId).refreshIndex();
    }



    private EntityIndex getManagementIndex() {

        return
            managerCache.getEntityIndex( // management app
                CpNamingUtils.getApplicationScope(getManagementAppId()));
    }




    @Override
    public void flushEntityManagerCaches() {

        managerCache.invalidate();

        applicationIdCache.evictAll();

        Map<UUID, EntityManager>  entityManagersMap = entityManagers.asMap();
        for ( UUID appUuid : entityManagersMap.keySet() ) {
            EntityManager em = entityManagersMap.get(appUuid);
            em.flushManagerCaches();
        }
    }


    @Override
    public Health getEntityStoreHealth() {

        // could use any collection scope here, does not matter
        EntityCollectionManager ecm = managerCache.getEntityCollectionManager(
            new ApplicationScopeImpl( new SimpleId( CpNamingUtils.MANAGEMENT_APPLICATION_ID, "application" ) ) );

        return ecm.getHealth();
    }



    @Override
    public Health getIndexHealth() {

       return getManagementIndex().getIndexHealth();
    }

    @Override
    public void initializeManagementIndex(){
        getManagementIndex().initialize();
    }
}
